Skip to content

fix(ios): prompt for Local Network access on .local and RFC1918 connections#1039

Merged
datlechin merged 1 commit intomainfrom
fix/ios-local-network-permission
May 6, 2026
Merged

fix(ios): prompt for Local Network access on .local and RFC1918 connections#1039
datlechin merged 1 commit intomainfrom
fix/ios-local-network-permission

Conversation

@datlechin
Copy link
Copy Markdown
Member

Summary

iOS users could not connect to MySQL/PostgreSQL/Redis servers (or SSH tunnels) on the local network using .local hostnames or RFC1918 addresses. iOS silently dropped every outbound connect() because the bundle was missing NSLocalNetworkUsageDescription and NSBonjourServices, so the system never showed the Local Network permission prompt. Reported by an iOS feedback tester for Seans-MacBook-Pro.local:22 SSH tunnel; surfaced as the misleading MySQL connection failed: Lost connection to server at 'handshake: reading initial communication packet', system error: 60 (errno 60 = ETIMEDOUT).

What changed

  • Info.plist: declare NSLocalNetworkUsageDescription (purpose string) and NSBonjourServices (_ssh._tcp, _mysql._tcp, _postgresql._tcp, _redis._tcp).
  • New LocalNetworkPermission actor (Platform/LocalNetworkPermission.swift): briefly starts an NWBrowser for _ssh._tcp the first time a connection targets a local-network host (Apple DTS "Local Network Privacy FAQ" pattern; a bare connect() does not always trigger the consent prompt for getaddrinfo-based connections). Watches the NWBrowser.State stream and resolves to .granted / .unavailable. Caches the resolution per process and coalesces concurrent first-time calls via an in-flight Task so parallel SSH + DB connects do not double-prompt.
  • Fail-fast on denial: subsequent connect() attempts throw LocalNetworkPermissionError.unavailable immediately instead of waiting for the 10s TCP timeout. Wired through SSHTunnelFactory.create() and MySQLDriver / PostgreSQLDriver / RedisDriver connect(). Loopback (127.0.0.1, ::1, localhost) and non-local hosts no-op the gate.
  • Host classifier: isLocalNetworkHost(_:) covers .local, IPv4 RFC1918 (10/8, 172.16-31/12, 192.168/16), IPv4 link-local (169.254/16), IPv6 ULA (fc00::/7), and IPv6 link-local (fe80::/10).
  • ErrorClassifier: matches the typed LocalNetworkPermissionError directly with a dedicated "Local Network Access Required" title. Falls back to detecting ETIMEDOUT (system error: 60, operation timed out) on SSH-enabled or local-network connections, replacing the generic "server is not responding" / misleading SSH-handshake copy with "Open Settings > Privacy & Security > Local Network and turn TablePro on."
  • Reorder classify() so the timeout signature wins over the substring "handshake" keyword that was previously routing MySQL ETIMEDOUT errors into the SSH branch.

Test plan

Run on a real iPhone (Simulator does not enforce Local Network privacy).

  • Cold first-launch prompt fires. Delete app, reinstall, create MySQL connection with SSH Tunnel ON to Some-Mac.local, tap Test Connection. iOS prompt appears with the purpose string. Allow → connect proceeds.
  • Deny -> fail-fast. Delete, reinstall, Test Connection to .local host, tap Don't Allow. Error appears in under 1 second with title "Local Network Access Required" and Settings copy.
  • Settings toggle recovers. After deny, toggle on in Settings -> Privacy & Security -> Local Network, force-quit, reopen, retest. Connects without re-prompting.
  • Direct .local connect (no tunnel). Driver-side gate fires for plain MySQL/Postgres/Redis to Some-Mac.local.
  • RFC1918 hosts. Test 192.168.x, 10.x, 172.16-31.x at least once each across SSH and direct paths.
  • Loopback no-ops. 127.0.0.1 / localhost connections never prompt.
  • Public host no-ops. db.example.com connects normally with no prompt.
  • Concurrent connects do not double-prompt. SSH Tunnel ON + DB connect: exactly one prompt.
  • Second connect after Allow. No re-prompt; instant proceed.
  • IPv6 link-local. fe80::xxxx%en0 triggers gate (if you have a target).
  • swiftlint lint --strict clean on touched files (verified locally).

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin datlechin merged commit 76b6ecf into main May 6, 2026
2 checks passed
@datlechin datlechin deleted the fix/ios-local-network-permission branch May 6, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant